package com.smartalarmclock.Model;

import java.util.HashSet;
import java.util.Hashtable;
import java.util.Set;

import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;

import com.smartalarmclock.Model.Exceptions.DataEntityDoesNotExistException;

public final class SmartAlarmClockDB {

	// Members
	
	private static final String c_ReferencesTableName = "ReferenceMapping";
	private static final String c_ReferencesColumnNameReferencingType = "Referencing";
	private static final String c_ReferencesColumnNameReferencedType = "Referenced";
	private static final String[] c_QueryAllColumns = new String[] { c_ReferencesColumnNameReferencedType, c_ReferencesColumnNameReferencingType };
	
	private static SmartAlarmClockDB m_Instance;
	private Hashtable<Class<?>, IDataEntityFactory> m_Factories;
	private Hashtable<Class<?>, Set<Class<?>>> m_ReferenceMappings;
	private boolean m_IsMappingLoaded;
	
	/**
	 * Private Constructor for the singleton
	 */
	private SmartAlarmClockDB() {
		m_ReferenceMappings = new Hashtable<Class<?>, Set<Class<?>>>();
		m_IsMappingLoaded = false;
		
		m_Factories = new Hashtable<Class<?>, IDataEntityFactory>();
		CreateDataEntityFactories();
	}

	/**
	 * @return the single instance of the DB
	 */
	public static SmartAlarmClockDB GetInstance() {
		if (m_Instance == null) {
			m_Instance = new SmartAlarmClockDB();
		}
		
		return m_Instance;
	}
	
	// Data Entity Factory Methods
	
	protected IDataEntityFactory GetDataEntityFactory(Class<?> dataEntityType) {
		if (!m_Factories.containsKey(dataEntityType)) {
			return null;
		}
		
		return m_Factories.get(dataEntityType);
	}
	
	// References Methods
	
	/**
	 * Adds a reference mapping and saves it in the DB.
	 * @param context the context of the request
	 * @param referencingEntityType the referencing type
	 * @param referencedEntityType the referenced type
	 */
	protected void AddReferenceMapping(Context context, Class<?> referencingEntityType, Class<?> referencedEntityType) {
		if (!m_ReferenceMappings.containsKey(referencedEntityType)) {
			m_ReferenceMappings.put(referencedEntityType, new HashSet<Class<?>>());
		}
		
		m_ReferenceMappings.get(referencedEntityType).add(referencingEntityType);
		SaveReferenceMapping(context, referencingEntityType, referencedEntityType);
	}
	
	/**
	 * Loads the referencing entities for the given referenced entity
	 * @param context the context of the request
	 * @param referencedEntity the referenced entity
	 * @return a set of all referencing entities of the referenced entity
	 * @throws DataEntityDoesNotExistException if the referencing entities do not exist. If this is thrown
	 * something went wrong with the handling of the references.
	 */
	protected Set<IReferencingDataEntity> LoadReferencingEntities(Context context, IReferencedDataEntity referencedEntity) throws DataEntityDoesNotExistException {
		LoadReferenceMapping(context);
		
		Set<IReferencingDataEntity> referencingEntities = new HashSet<IReferencingDataEntity>();
		for (Class<?> referencedType : m_ReferenceMappings.keySet()) {
			if (referencedType.isInstance(referencedEntity)) {
				for (Class<?> referencingType : m_ReferenceMappings.get(referencedType)) {
					referencingEntities.addAll(
						((IReferencingDataEntityFactory)(GetDataEntityFactory(referencingType))).
							GetReferencingEntities(context, referencedEntity));
				}
			}
		}
		
		return referencingEntities;
	}
	
	/**
	 * @return the CREATE TABLE statement of the References table
	 */
	protected static String GetSqlStatementCreateReferencesTable() {
		return "CREATE TABLE " + c_ReferencesTableName + 	" (" +
				c_ReferencesColumnNameReferencedType + 	" TEXT NOT NULL, " +
				c_ReferencesColumnNameReferencingType + " TEXT NOT NULL, " +
				"PRIMARY KEY (" + c_ReferencesColumnNameReferencedType + ", " + c_ReferencesColumnNameReferencingType + "))";
	}
	
	/**
	 * @return the DROP TABLE statement of the References table
	 */
	protected static String GetSqlStatementDropReferencesTable() {
		return "DROP TABLE IF EXISTS " + c_ReferencesTableName;
	}
	
	// Private Methods
	
	/**
	 * Loads to memory all the reference mappings from the DB.
	 * @param context the context of the request
	 */
	private void LoadReferenceMapping(Context context) {
		if (!m_IsMappingLoaded) {
			SQLiteDatabase db = DataEntity.OpenDBForReading(context);
			Cursor cursor = db.query(c_ReferencesTableName, c_QueryAllColumns, null, null, null, null, null);
			
			Class<?> referencingType = null;
			Class<?> referencedType = null;
			while (cursor.moveToNext()) {
				referencedType = GetModelClass((cursor.getString(cursor.getColumnIndex(c_ReferencesColumnNameReferencedType))));
				referencingType = GetModelClass((cursor.getString(cursor.getColumnIndex(c_ReferencesColumnNameReferencingType))));
				
				if (!m_ReferenceMappings.containsKey(referencedType)) {
					m_ReferenceMappings.put(referencedType, new HashSet<Class<?>>());
				}
				
				m_ReferenceMappings.get(referencedType).add(referencingType);
			}
			
			cursor.close();
			db.close();
			
			m_IsMappingLoaded = true;
		}
	}
	
	/**
	 * Creates all the data entity factories
	 */
	private void CreateDataEntityFactories() {
		IDataEntityFactory contactFactory = new ContactFactory();
		m_Factories.put(Contact.class, contactFactory);
		m_Factories.put(SingleContact.class, contactFactory);
		m_Factories.put(Group.class, contactFactory);
		
		IDataEntityFactory rulesItemFactory = new RulesItemFactory();
		m_Factories.put(RulesItem.class, rulesItemFactory);
		m_Factories.put(Profile.class, rulesItemFactory);
		m_Factories.put(VIP.class, rulesItemFactory);
		
		m_Factories.put(Snooze.class, new SnoozeFactory());
		m_Factories.put(AlarmClock.class, new AlarmClockFactory());
		m_Factories.put(SMSMessage.class, new SMSMessageFactory());
		m_Factories.put(AutoResponse.class, new AutoResponseFactory());
		m_Factories.put(TimeFrame.class, new TimeFrameFactory());
		m_Factories.put(LogEntry.class, new LogEntryFactory());
	}
	
	/**
	 * Saves the referencing types in the database. This way the referencing types
	 * can be loaded again.
	 * @param context the context of the request
	 * @param referencingType the referencing type
	 * @param referencedType the referenced type
	 */
	private void SaveReferenceMapping(Context context, Class<?> referencingType, Class<?> referencedType) {
		SQLiteDatabase db = DataEntity.OpenDBForWriting(context);
		
		// Checking if the mapping exists
		Cursor cursor = db.query(
			c_ReferencesTableName,
			c_QueryAllColumns,
			c_ReferencesColumnNameReferencedType + " LIKE \"" + referencedType.getSimpleName() + "\" AND " + c_ReferencesColumnNameReferencingType + " LIKE \"" + referencingType.getSimpleName() + "\"",
			null, null, null, null);
		
		if (!cursor.moveToNext()) {
			// If no records were queried, add the reference mapping
			ContentValues values = new ContentValues();
			values.put(c_ReferencesColumnNameReferencedType, referencedType.getSimpleName());
			values.put(c_ReferencesColumnNameReferencingType, referencingType.getSimpleName());
			
			db.insert(c_ReferencesTableName, null, values);
		}
		
		cursor.close();
		db.close();
	}
	
	private Class<?> GetModelClass(String simpleClassName) {
		String modelPackageName = getClass().getPackage().getName();
		try {
			return Class.forName(modelPackageName + "." + simpleClassName);
		} catch (ClassNotFoundException e) {
			// Cannot happen since the values are created from actual class names.
		}
		
		return null;
	}
}
